package net.xdclass.onlinexdclass.aop;

import com.alibaba.druid.support.json.JSONUtils;
import lombok.extern.java.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 第一步导入maven依赖
 *
 * @author xuehu
 * @date 2020/12/10 15:37
 * 定义切面
 */
@Aspect
@Component
@Log
public class AspectConfig {

    //定义切入点
    @Pointcut("execution(public * net.xdclass.onlinexdclass.controller.UserController.login(..))")
    private void index_log() {
    }

    /**
     * 记录HTTP请求结束时的日志
     * 开始前执行
     * 切入点表达式
     * 定义切入点的时候需要一个包含名字和任意参数的签名，还有一个切入点表达式，如@Pointcut("execution(public * com.softdev.system.demo.controller.DemoController.index(..))")
     * <p>
     * 意思是，指定了DemoController的index方法。
     * <p>
     * 切入点表达式的格式为
     * <p>
     * execution([可见性]返回类型[声明类型].方法名(参数)[异常])
     * <p>
     * 其中[]内的是可选的，其它的还支持通配符的使用：
     * <p>
     * *：匹配所有字符
     * ..：一般用于匹配多个包，多个参数
     * +：表示类及其子类
     * 运算符有：&&,||,!
     * 切入点指示符
     * execution - 匹配方法执行的连接点，这是你将会用到的Spring的最主要的切入点指示符。
     * <p>
     * within - 限定匹配特定类型的连接点（在使用Spring AOP的时候，在匹配的类型中定义的方法的执行）。
     * <p>
     * this - 限定匹配特定的连接点（使用Spring AOP的时候方法的执行），其中bean reference（Spring AOP 代理）是指定类型的实例。
     * <p>
     * target - 限定匹配特定的连接点（使用Spring AOP的时候方法的执行），其中目标对象（被代理的应用对象）是指定类型的实例。
     * <p>
     * args - 限定匹配特定的连接点（使用Spring AOP的时候方法的执行），其中参数是指定类型的实例。
     *
     * @target - 限定匹配特定的连接点（使用Spring AOP的时候方法的执行），其中正执行对象的类持有指定类型的注解。
     * @args - 限定匹配特定的连接点（使用Spring AOP的时候方法的执行），其中实际传入参数的运行时类型持有指定类型的注解。
     * @within - 限定匹配特定的连接点，其中连接点所在类型已指定注解（在使用Spring AOP的时候，所执行的方法所在类型已指定注解）。
     * @annotation - 限定匹配特定的连接点（使用Spring AOP的时候方法的执行），其中连接点的主题持有指定的注解。
     * <p>
     * 当然，其中execution使用最频繁也是最方便，即某方法执行时进行切入。
     */
    @Before("index_log()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        //接收到请求,记录请求内容
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        //记录以下内容
        log.info(">>>>>>>Before");
        log.info("URL:" + request.getRequestURL());
        log.info("HTTP_METHOD:" + request.getMethod());
        log.info("IP:" + request.getRemoteAddr());
        log.info("PATH:" + request.getServletPath());
        log.info("METHOD:" + request.getMethod());
        //获取拦截的方法名
        log.info("CLASS_METHOD:" + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
        log.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
    }

    /**
     * @param joinPoint
     * @After后置最终通知 当某连接点退出时执行的通知（不论是正常返回还是异常退出）。
     */
    @After("index_log()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("调用后执行After");
    }

    /**
     * @param obj
     * @throws Throwable
     * @AfterReturning后置返回通知 在某连接带你之后执行的通知, 通常在一个匹配的方法返回的时候执行(可以在后置通知中绑定返回值)
     * 1.如果参数中的第一个参数为JoinPoint,则第二个参数为返回值的信息。
     * 2.AfterReturning限定了只有目标方法返回值与通知方法相同的类型的参数时才能执行后置返回通知,否则不执行。
     * 3.对于returning对应的通知方法参数为Object类型将匹配任何目标返回值。
     */
    @AfterReturning(returning = "obj", pointcut = "index_log()")
    public void doAfterReturning(Object obj) throws Throwable {
        //处理完请,返回内容
        log.info(">>>>>>AfterReturning");
        log.info("RESPONSE" + JSONUtils.toJSONString(obj));
    }

    /**
     * @param joinPoint
     * @param exception
     * @AfterThrowing后置异常通知 在方法抛出异常退出时执行的通知
     */
    @AfterThrowing(value = "index_log()", throwing = "exception")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable exception) {
        //目标方法名:
        log.info(">>>>>>AfterThrowing>>>>>>>>");
        log.info(joinPoint.getSignature().getName());
        if (exception instanceof NullPointerException) {
            log.info("发生了空指针异常");
        } else {
            log.info("发生了未知异常");
        }
    }

    /**
     * @param proceedingJoinPoint
     * @return
     * @Around环绕通知 包围一个连接点的通知, 如方法调用等, 这是最强大也是最麻烦的一种通知类型, 可以在方法调用前后
     * 完成自定义的行为,它也会选择是否继续执行连接点或者直接返回它自己的返回值或抛出异常来结束执行。
     * <p>
     * 对方法的环绕,具体方法会通过代理传递到切面中去,切面中可选择执行方法与否,执行几次方法等,环绕通知使用一个代理
     * ProceedingJoinPoint类型的对象来管理目标对象,所以此通知的第一个参数必须是ProceedingJointPoint类型。
     * 在通知体内调用ProceedingJoinPoint的proceed()方法会导致后台的连接点方法执行。
     * proceed()方法也可能会被调用并且传入一个Object[]对象，该数组中的值将被作为方法执行时的入参。
     */
    @Around(value = "index_log()")
    public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
        log.info(">>>>>>>Around");
        log.info("环绕通知的目标方法名:" + proceedingJoinPoint.getSignature().getName());
        try {
            Object proceed = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }
}
